Comencemos con una introducción rapida a C. Nuestro objetivo es mostrar los elementos esenciales del lenguaje en programas reales, pero sin perdernos en detalles, reglas o excepciones. Por el momento, no intentamos ser completos ni precisos (exceptuando en los ejemplos que sí lo son). Deseamos llevarlo tan rápido como sea posible al punto en donde pueda escribir programas útiles, y para hacerlo tenemos que concentrarnos en las bases: variables y constantes, aritmética, control de flujo, funciones y los rudimentos de entrada y salida. Hemos dejado intencionalmente fuera de este capítulo las características de C que son importantes para escribir programas más grandes. Esas características incluyen apuntadores, estructuras, la mayor parte del rico conjunto de operadores de C, varias proposiciones para control de flujo y la biblioteca estándar.
Este enfoque tiene sus inconvenientes. Lo más notorio es que aqui no se encuentra la descripción completa de ninguna característica particular del lenguaje, y la introducción, por su brevidad, puede también ser confusa. Y debido a que los ejemplos no utilizan la potencia completa de C, no son tan concisos y elegantes como podrían serlo. Hemos tratado de aminorar esos efectos, pero tenga cuidado. Otro inconveniente es que los capítulos posteriores necesariamente repitirán algo de lo expuesto en éste. Esperamos que la repetición, más que molestar, ayude.
En cualquier caso, los programadores con experiencia deben ser capaces de extrapolar del material que se encuentra en este capítulo a sus propias necesidades de programación. Los principiantes deben complementarlo escribiendo pequeños programas semejantes a los aquí expuestos. Ambos grupos pueden utilizar este capítulo como un marco de referencia sobre el cual asociar las descripciones más detalladas que comienzan en el capítulo 2.
La única forma de aprender un nuevo lenguaje de programación es escribiendo programas en él. El primer programa por escribir es el mismo para todos los lenguajes:
Imprima las palabrashola, mundo
Este es el gran obstáculo; para librarlo debe tener la habilidad de crear el texto de programa de alguna manera, compilarlo con éxito, cargarlo, ejecutarlo y descubrir a dónde fue la salida. Con el dominio de estos mecánicos, todo lo demas es relativamente fácil.
En C, el programa para escribir "hola, mundo" es
#include <stdio.h>
main()
{
printf("hola, mundo\n");
}
La forma de ejecutar este programa depende del sistema que se
esté utilizado. Como un ejemplo específico, en el
sistema operativo UNIX se debe crear el programa en un archivo cuyo
nombre termine con ".c", como hola.c, y después
compilarlo con la orden
cc hola.c
Si no se ha cometido algún error, como la omisión
de un carácter o escribir elgo en forma incorrecta, la
compilación se hará sin emitir mensaje alguno, y
creará un archivo ejecutable llamado a.out. Si
se ejecuta a.out escribiendo la orden
a.out
se escribirá
hola, mundo
En otras sistemas, las reglas serán diferentes, consúltelo con un experto.
Ahora algunas explicaciones acerca del programa en sí. Un
programa en C, cualquiera que sea su tamaño, consta de
funciones y variables. Una función contiene
proposiciones que especifican las operaciones de
cálculo que se van a realizar, y las variables almacenan los
valores utilizados durante los cálculos. Las funciones de C
son semejantes a las subrutinas y funciones de Fortran o a los
procedimientos y funciones de Pascal. Nuestro ejemplo es una
función llamada main. Normalmente se tiene la
libertad de dar cualquier nombre que se desee, pero
"main" es especial-- el programa comienza a ejecutarse
al principio de main. Esto significa que todo programa
debe tener un main en algún sitio.
Por lo común main llamará a otras
funciones que ayuden a realizar su trabajo, algunas que usted ya
escribió, y otras de bibliotecas escritas previamente. La
primera línea del programa:
#include <stdio.h>
indica al compilador que debe incluir información acerca de la biblioteca estándar de entrada/salida; esta línea aparece al principio de muchos archivos fuente de C. La biblioteca estándar está descrita en el capítulo 7 y en el apéndice B.
Un método para comunicar datos entre las funciones es que
la función que llama proporciona una lista de valores,
llamados argumentos a la función que está
invocando. Los paréntesis que están después
del nombre de la función encierran a la lista de argumentos.
En este ejemplo, main está definido para ser
una función que no espera argumentos, lo cual está
indicado por la lista vacía ().
#include <stdio.h> incluye información acerca de la biblioteca estándar main() define una función llamadamainque no recibe valores de argumentos { las proposiciones demainestán encerradas entre llaves printf("hola, mundo\n");mainllama a la función de bibliotecaprintf.} para escribir esta secuencia de caracteres;\nrepresenta el carácter nueva línea
Las proposiciones de una función están encerradas
entre llaves {}. La función main sólo
contiene una proposición,
printf("hola, mundo\n");
Una función se invoca al nombrarlo, seguida de una lista
de argumentos entre paréntesis; de esta manera se
será llamando a la función printf con el
argumento "hola, mundo\n". printf es una
función de biblioteca que escribe la salida, en este caso la
cadena de caracters que se encuentra entre comillas.
A una secuencia de caracteres entre comillas, como "hola,
mundo\n", se le llama cadena de caracteres o
constante de cadena. Por el momento, nuestro único
uso de cadenas de caracteres será como argumentos para
printf y otras funciones.
La secuencia \n en la cadena representa el
carácter nueva línea en la notación de
C, y hace avanzar la impresión al margen izquierdo de la
siguente línea. Si se omite el \n (un
experimento que vale la pena), encontrará que no hay avance
de línea después de la impresión. Se debe
utilizar \n para incluir un carácter nueva
línea en el argumento de printf; si se intenta
algo como
printf ("hola, mundo
");
el compilador de C producirá un mensaje de error.
printf nunca propociona una nueva línea
automáticamente, de manera que se pueden utilizar varias
llamadas para construir una línea de salida en etapas.
Nuestro primer programa también pudo haber sido escrito de
la siguiente manera.
#include <stdio.h>
main()
{
printf("hola, ");
printf("mundo");
printf("\n");
}
produciendo una salida idéntica.
Nótese que \n representa un solo
carácter. Una secuencia de escape como
\n proporciona un mecanismo general y extensible para
representar caracteres invisibles o difíciles de escribir.
Entre otros que C proporciona están \t para
tabulación, \b para retroceso, \"
para comillas, y \\ para la diagonal invertida. Hay
una lista completa en la sección
2.3.
Ejercicio 1-1. Ejecute el programa "hola,
mundo" en su sistema. Expirimente con la omisión de
partes del programa, para ver qué mensajes de error se
obtienen.
Ejercicio 1-2. Expirimente el descubrir qué pasa
cuando la cadena del argumento de printf contiene
\c, en donde c es algún
carácter no puesto en lista anteriormente.
El siguiente programa utiliza la fórmula °C = (5/9) (°F-32) para imprimir la siguiente tabla de temperaturas Farenheit y sus equivalentes centigrados o Celcius:
0 -17 20 -6 40 4 60 15 80 26 100 37 120 48 140 60 160 71 180 82 200 93 220 104 240 115 260 126 280 137 300 148
En sí el programa aún consiste de la
definición de una única función llamada
main. Es más largo que el que imprime
"hola, mundo", pero no es complicado. Introduce varias
ideas nuevas, incluyendo comentarios, declaraciones, variables,
expresiones aritméticas, ciclos y salida con formato.
#include <stdio.h>
/* imprime la tabla Farenheit-Celsius
para fahr = 0, 20, ..., 300 */
main()
{
int fahr, celsius;
int lower, upper, step;
lower = 0; /* límite
inferior de la tabla
de temperatureas */
upper = 300; /* límite
superior */
step = 20; /* tamaño
del incremento */
fahr = lower;
while (fahr <= upper) {
celsius = 5 * (fahr-32) / 9;
printf("%d\t%d\n",
fahr, celsius);
fahr = fahr + step;
}
}
Las dos líneas
/* imprime la tabla Farenheit-Celsius para fahr = 0, 20, ..., 300 */
son un comentario que en este caso explica brevemente lo que hace el programa. Cualesquier caracteres entre /* y */ son ignorados por el compilador, y pueden ser utilizados libremente para hacer a un programa más fácil de entender. Los comentarios pueden aparacer en cualquier lugar donde puede colocarse un espacio en blanco, un tabulador o nueva línea.
En C, se deben declarar todas las variables antes de su uso, generalmente al principio de la función y antes de cualquier proposición ejecutable. Una declaración notifica las propiedade de una variable; consta de un nombre de tipo y una lista de variables, como
int fahr, celsius; int lower, upper, step;
El tipo int significa que las variables de la lista
son enteros, en contraste con float, que significa
punto flotante, esto es, números que pueden tener una parte
fraccionaria. El rango tanto de int como de
float depende de la máquina que se está
utilizando; los int de 16 bits, que están
comprendidos entre el -32768 y +32767, son comunes, como son los
int de 32 bits. Un número float
típicamente es de 32 bits, por lo menos con seis
dígitos significativos y una magnitud generalmente entre
10-38 y 10+38.
Adamás de int y float, C
proporciona varios tipos de datos básicos, incluyendo:
char carácter --un solo byte
short entero corto
long entero largo
double punto flotante de doble
precisión
Los tamaños de estos objetos también dependen de la máquina. También existen arreglaos, estructuras y uniones de estos tipos básicos, apuntadores a ellos y funciones que regresan valores con esos tipos, todo lo cual se verá en el momento oportuno.
Los cálculos en el programa de conversión de temperaturas principian con las proposiciones de asignación.
lower = 0; upper = 300; step = 20; fahr = lower,
que asignan a las variables sus valores iniciales. Las proposiciones individuales se terminan con punto y coma.
Cada línea de la tabla se cálcula de la misma
manera por lo que se utiliza una iteración que se repite una
vez por cada línea de salida; este es el proposito del ciclo
while
while (fahr <= upper) {
...
}
El ciclo while funciona de la siguiente manera: se
prueba la condición entre paréntesis. De ser
verdadera (fahr es menor o igual que
upper), el cuerpo del ciclo (las tres proposiciones
entre llaves) se ejecuta de nuevo. Cuando la prueba resulta falsa
(fahr excede a upper) la iteración
termina, y la ejecución continúa en la
proposición que sigue al ciclo. No existe ninguna otra
proposición en este programa, de modo que termina.
El cuerpo de un while puede tener una o más
proposiciones encerradas entre llaves, como en el convertidor de
temperaturas, o una sola proposición sin llaves, como en
while (i < j) i = 2 * i;
En cualquier caso, siempre se sangra la proposición
controlada por el while con una tabulación (lo
que se ha mostrado con cuatro espacios) para poder apreciar de un
vistazo cuáles proposiciones están dentro del ciclo.
Aunque los compiladores de C no les importa la apariencia del
programa, un sangrado y espaciamiento adecuados son muy importantes
para hacer programas fáciles de leer. Se recomienda escribir
una sola proposición por línea y utilizar espacios en
blanco alrededor de los operadores para dar claridad al
agrupamiento. La posición de las llaves es menos importante,
aunque la gente mantiene creencias apasionadas. Se eligió
uno de los varios estilos populares. Seleccione un estilo que le
satisfaga y úselo en forma consistente.
La mayor parte del trabajo se realiza en el cuerpo del ciclo. La
temperatura Celsius se cálcula y se asigna a la variable
celsius por la proposición.
celsius = 5 * (fahr-32) / 9;
La razón de multiplicar por 5 y después dividir entre 9 en lugar de solamente multiplicar por 5/9 es que en C, como en muchos otros lenguajes, la división de enteros trunca el resultado: cualquier parte fraccionaria de descarta. Puesto que 5 y 9 son enteros, 5/9 sería truncado a cero y así todas las temperatura Celsius se reportarían como cero.
Este ejemplo también muestra un poco más acerca de
cómo funciona printf. En realidad,
printf es una función de propósito
general para dar formato de salida, que se describirá con
detalle en el capítulo 7. Su primer argumento es una cadena
de caracteresque serán impresos, con cada % indicando en
donde uno de los otros (segundo, tercero, ...) argumentos va a ser
sustituido, y en qué forma será impreso. Por ejemplo,
%d especifica un argumento entero, de modo que la
proposición
printf("%d\t%d\n", fahr, celsius);
hace que los valores de los dos enteros fahr y
celsius sean escritos, con una tabulación
(\t) entre ellos.
Cada construcción % en el primer argumento de
printf está asociada con el correspondiente
segundo argumento, tercero, etc., y deben corresponder
apropiadamente en número y tipo, o se tendrán
soluciones incorrectas.
Con relación a esto, printf no es parte del
lenguaje C; no existe propiamente una entrada o salida definida en
C. printf es sólo una útil
función de la biblioteca estándar de funciones que
está accesible normalmente a los programas en C. Sin
embargo, el comportamiento de printf está
definido en el estándar ANSI, por lo que sus propiedades
deben ser las mismas en cualquier compilador o biblioteca que se
apegue a él.
Para concentrarnos en C, no hablaremos mucho acerca de la
entrada y la salida hasta el capítulo
7. En particular, pospondremos el tema de la entrada con
formato hasta entonces. Si se tiene darle entrada a números,
léase la sicusión de la función
scanf en la sección
7.4. La función scanf es como
printf, exceptuando que lee de la entrada en lugar de
escribir a la salida.
Existen un par de problemas con el programa de conversión
de temperaturas. El más simple es que la salida no es muy
estética debido a que los números no están
justificados hacia su derecha. Esto es fácil de corregir; si
aumentamos a cada %d de la proposición
printf una amplitud, los números impresos
serán justificados hacia su derecho dentro de sus campos.
Por ejemplo, podría decirse
printf("%3d %6d\n", fahr, celsius);
para escribir el primer número de cada línea en un campo de tres dígitos de ancho, y el segundo en un campo de seis dígitos, como esto:
0 -17 20 -6 40 4 60 15 80 26 100 37 ...
El problema más grave es que debido a que se ha utilizado aritmética de enteros, las temperaturas Celsius no son muy precisas; por ejemplo, 0°F es en realidad aproximadamente -17.8°C, no -17. Para obtener soluciones más precisas, se debe utilizar aritmética de punto flotante en lugar de entera. Esto requiere de algunos cambios en el programa. Aquí está una segunda versión:
#include <stdio.h>
/* imprime la tabla Fahrenheit-Celsius
para fahr = 0, 20, ..., 300;
versión punto flotante */
main()
{
float fahr, celsius;
int lower, upper, step;
lower = 0; /* límite superior
de la tabla de
temperaturas */
upper = 300; /* límite
superior */
step = 20; /* tamaño del
incremento */
fahr = lower;
while (fahr <= upper) {
celsius = (5.0/9.0) *
(fahr-32.0);
printf("%3.0f %6.1f\n",
fahr, celsius);
fahr = fahr + step;
}
}
Esto es muy semejante a lo anterior, excepto que
fahr y celsius están declarados
como float, y la fórmula de conversión
está escrita en una forma más natural. No pudimos
utilizar 5/9 en la versión anterior debido a que la
división entera lo truncaría a cero. Sin embargo, un
punto decimal en una constante indica que ésta es de punto
flotante, por lo que 5.0/9.0 no se trunca debido a que es una
relación de dos valores de punto flotante.
Si un operador aritmético tiene operandos enteros, se
ejecuta una operación entera. Si un operador numérico
tiene un operando de punto flotante y otro entero, este
último será convertido a punto flotante antes de
hacer la operación. Si hubiera escrito fahr -
32, el 32 sería convertido
automáticamente a punto flotante. Escribir constantes de
punto flotante con puntos decimales explícitos, aun cuando
tengan valores enteros, destaca su naturaleza de punto flotante
para los lectores humanos.
Las reglas detalladas de cuándo los enteros se convierten a punto flotante se encuentran en el capítulo 2. Por ahora, nótese que la asignación
fahr = lower;
y la prueba
while (fahr <= upper)
también trabajan en la forma natural --el
int se convierte a float antes de
efectuarse la operación.
La especificación de conversión %3.0f
del printf indica que se escribirá u numero de
punto flotante (en este caso fahr) por lo menos con
tres caracteres de ancho, sin punto decimal y sin dígitos
fraccionarios; %6.1f describe a otro número
(celsius) que se escribirá en una amplitud de
por lo menos 6 caracteres, con 1 dígito después del
punto decimal. La salida se verá como sigue:
0 -17.8 20 -6.7 40 4.4 ...
La amplitud y la precisión pueden omitirse de una
especificación: %6f indica que el número
es por lo menos de seis caracteres de ancho; %.2f
indica dos caracteres después del punto decimal, pero el
ancho no está restringado; y %f
únicamente indica escribir el número como punto
flotante.
%d escribe como entero decimal
%6d escribe como entero decimal,
por lo menos con 6 caracteres
de amplitud
%f escribe como punto flotante
%6f escribe como punto flotante,
por lo menos con 6 caracteres
de amplitud
%.2f escribe como punto flotante,
con 2 caracteres después del
punto decimal
%6.2f escribe como punto flotante,
por lo menos con 6 crácteres
de ancho y 2 después del
punto decimal
Entre otros, printf también reconoce
%o para octal, %x para hexadecimal,
%c para carácter, %s para cadena
de caracteres y %% para % en sí.
Ejercicio 1-3 Modifique el programa de conversión
de temperaturas de modo que escriba un encabezado sobre la tabla.
Ejercicio 1-4 Escriba un programa que imprima la tabla
correspondiente Celsius a Fahrenheit.
Existen suficientes formas distintas de escribir un programa para una tarea en particular. Intentemos una variación del programa de conversión de temperaturas.
#include <stdio.h>
/*imprime la tabla Fahrenheit-Celsius */
main()
{
int fahr;
for (fahr = 0; fahr <= 300; fahr = fahr + 20)
printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}
Este produce los mismos resultados, pero ciertamente se ve
diferente. Un cambio importante es la eliminación de la
mayoría de las variables; sólo permanece
fahr y la hemos hecho int. Los
límites inferior y superior y el tamaño del avance
sólo aparecen como constantes dentro de la
proposición for, que es una nueva
construcción, y la expresión que cálcula la
temperatura Celcius ahora aparece como el tercer argumento de
printf en vez de una proposición de
asignación separada.
Este último cambio ejmplifica una regla general --en
cualquier contexto en el que se permita utilizar el valor de una
variable de algún tipo, es posible usar una expresión
más complicada de ese tipo. Puesto que el tercer argumento
de printf debe ser un valor de punto flotante para
coincidir con %6.1f, cualquier expresión de
punto flotante puede estar allí.
La proposición for es un ciclo, una forma
generalizada del while. Si se compara con el
while anterior, su operación debe ser clara.
Dentro de los paréntess existen tres secciones, separadas
por punto y coma. La primera, la inicialización
fahr = 0
se ejecuta una vez, antes de entrar propiamente al ciclo. La segunda sección es la condición o prueba que controla el ciclo:
fahr <= 300
Esta condición se evalúa; si es verdadera, el
cuerpo del ciclo (en este caso un simple printf) se
ejcuta. Después el incremento de avance
fahr = fahr + 20
se ejecuta y la condición se vuelve a evaluar. El ciclo
termina si la condición se hace falsa. Tal como con el
while, el cuerpo del ciclo puede ser una
proposición sencilla o un grupo de proposiciones encerradas
entre llaves. La inicialización, la condición y el
incremento pueden ser cualquier expresión.
La selección entre while y for
es arbitraria, y se basa en aquello que parezca más claro.
El for es por lo general apropiado para ciclos en los
que la inicialización y el incremento son proposiciones
sencillas y lógicamente relacionadas, puesto que es
más compacto que el while y mantiene reunidas
en un lugar a las proposiciones que controlan al ciclo.
Ejercicio 1-5. Modifique el programa de conversión
de temperaturas de manera que escriba la tabla en orden inverso,
esto es, desde 300 grados hasta 0.
Una observación final antes de dejar definitivamente el
tema de la conversión de temperaturas. Es una mala
práctica poner "números mágicos" como 300 y 20
en un programa, ya que proporcionan muy poca información a
quien tenga que leer el programa, y son difíciles de
modificar en un forma sistemática. Una manera de tratar a
esos números mágicos es darles nombres
significativos. Una línea #define define un
nombre simbólico o constante simbólica
como una cadena de carácteres especial:
#define nombre texto de reemplazo
A partir de esto, cualquier ocurrencia de nombre (que no esté entre comillas ni como parte de otro nombre) se sustituirá por el texto de reemplazo correspondiente. El nombre tiene la misma forma que un nombre de variable: una sequencia de letras y dígitos que comienza con una letra. El texto de reemplazo puede ser cualquier sequencia de carácteres; no está limitado a números.
#include <stdio.h>
#define LOWER 0 /* limite inferior
de la tabla */
#define UPPER 300 /* limite
superior */
#define STEP 20 /* tamaño del
incremento */
/* imprime la tabla
Fahrenheit-Celsius */
main()
{
int fahr;
for (fahr = LOWER; fahr <= UPPER;
fahr = fahr + STEP)
printf("%3d %6.1f\n", fahr,
(5.0/9.0)*(fahr-32));
}
Las cantidades LOWER, UPPER y STEP son
constantes simbólicos, no variables, por lo que no aparecen
entre las declaraciones. Los nombres de constantes
simbólicas, por convención se escriben con letras
mayúsculas, de modo que se puedan distinguir
fácilmente de los nombres de variables escritos con
minúsculas. Nótese que no hay punto y coma al final
de una línea #define.
Ahora vamos a considerar una familia de programas relacionados para el procesamiento de datos de tipo carácter. Se encontrará que muchos programas sólo son versiones ampliadas de los prototipos que se tratan aquí
El modelo de entrada y salida manejado por la biblioteca estándar es muy simple. La entrada y salida de texto, sin importar dónde fue originada o hacia dónde se dirige, se tratan como flujos (streams) de carácteres. Un flujo de texto es una sequencia de carácteres divididos entre líneas, cadauna de las cuales consta de cero o más caracteres seguidos de un carácter nueva línea. La biblioteca es responsable de hacer que cada sequencia de entrada o salida esté de acuerdo con este modelo; el programador de C qu utiliza la biblioteca no necesita preocuparse de cómo están representadas las líneas fuera del programa.
La biblioteca estándar proporciona varias funciones para
leer o escribir un carácter a la vez, de las cuales
getchar y putchar son las más
simples. Cada vez que se invoca, getchar lee el
siguiente carácter de entrada de una sequencia de
texto y lo devuelve como su valor. Esto es, después de
c = getchar()
la variable c contiene el siguiente
caráacter de entrada. Los carácteres provienen
normalmente del teclado; la entrada de archivos se trata en el capítulo 7.
La función putchar escribe un
carácter cada vez que se invoca:
putchar(c)
escribe el contenido de la variable entera c como
un carácter, generalmente en la pantalla. Las llamadas a
putchar y a printf pueden estar
alternadas; la salida aparecerá en el orden en que se
realicen las llamadas.
Con getchar y putchar se puede
escribir una cantidad sorprendente de código útil sin
saber nada más acerca de entrada y salida. El ejemplo
más sencillo es un programa que copia la entrada en la
salida, un carácter a la vez:
lee un carácter
while (carácter no es indicador
de fin de archivo)
manda a la salida el carácter
recién leído
lee un carácter
Al convertir esto en C se obtiene
#include <stdio.h>
/* copia la entrada a la salida;
la. versión */
main()
{
int c;
c = getchar();
while (c != EOF) {
putchar(c);
c = getchar();
}
El operador de relación != significa "no igual a".
Lo que aparece como un carácter en el teclado o en la
pantalla es, por supuesto, como cualquier otra cosa, almacenado
internamente como un patrón de bits. El tipo char tiene la
función específica de almacenar ese tipo de dato,
pero también puede ser usado cualquier tipo de entero.
Usamos int por una sutil pero importante
razón.
El problema es distinguir el fin de la entrada de los datos
válidos. La solución es que getchar devuelve un valor
distintivo cuando no hay más a la entrada, un valor que no
puede ser confundido con ningún otro carácter. Este
valor se llama EOF, por "end of file (fin de
archivo)". Se debe declarar c con un tipo que sea lo
suficientemente grande para almacenar cualquier valor que le
regrese getchar. No se puede utilizar char puesto que
c debe ser suficientemente grande como para mantener a
EOF además de cualquier otro carácter. Por lo tanto,
se emplea int.
EOF es un entero definido en
<stdio.h> , pero el valor numérico
específico no importa mientras que no sea el mismo que
ningún valor tipo char. Utilizando la constante
simbólica, hemos asegurado que nada en el programa depende
del valor numérico especifícado.
El programa para copiar podría escribirse de modo más conciso por programadores experimentados de C. En lenguaje C, cualquier asignación, tal como
c = getchar()
es una expresión y tiene un valor, el del lado izquierdo
luego de la asignación. Esto significa que una
asignación puede aparecer como parte de una expresión
más larga. Si la asignación de un carácter a c
se coloca dentro de la sección de prueba de un cicló
while, el programa que copia puede escribirse de la
siguiente manera:
#include <stdio.h>
/* copia la entrada a la salida; 2a. versión */
main()
{
int c;
while ((c = getchar()) != EOF)
putchar(c);
}
El while obtiene un carácter, lo asigna a
c, y entonces prueba si él carácter fue
la señal de fin de archivo. De no serlo, el cuerpo del while
se ejecuta, escribiendo el carácter; luego se repite el
while. Luego, cuando se alcanza el final de la
entrada, el while termina y también lo hace
main.
Esta versión centraliza la entrada -ahora hay sólo
una referencia a getchar- y reduce el programa. El
programa resultante es más compacto y más
fácil de leer una vez que se domina el truco. Usted
verá seguido este estilo. (Sin embargo, es posible
descarriarse y crear código impenetrable, una tendencia que
trataremos de reprimir.)
Los paréntesis que están alrededor de la
asignación dentro de la condición son necesarios. La
precedencia de != es más alta que la de
= , lo que significa que en ausencia de
paréntesis la prueba de relación != se
realizaría antes de la asignación =. De
esta manera, la proposición
c = getchar() != EOF
es equivalente a
c = (getchar() != EOF)
Esto tiene el efecto indeseable de hacer que c sea
O o 1, dependiendo de si la llamada de getchar
encontró fin de archivo. (En el capítulo 2 se trata
este tema con más detalle).
Ejercicio 1-6. Verifique que la expresión
getchar () != EOF es O o 1.
Ejercicio 1-7. Escriba un programa que imprima el valor
de EOF.
El siguiente programa cuenta caracteres y es semejante al programa que copia.
#include <stdio.h>
/* cuenta los caracteres de la
entrada; la. versión */
main()
{
long nc;
nc = 0;
while (getchar() != EOF)
++nc;
printf("%ld\n", nc);
}
La proposición
++nc;
presenta un nuevo operador, ++, que significa
incrementa en uno. Es posible escribir nc = nc +
1, pero ++ nc es más conciso y muchas
veces más eficiente. Hay un operador correspondiente
-- para disminuir en 1. Los operadores ++
y -- pueden ser tanto operadores prefijos
(++nc) como postfijos (nc++); esas dos
formas tienen diferentes valores dentro de las expresiones, como se
demostrará en el capitulo 2, pero ambos ++nc y
nc++ incrementan a nc. Por el momento
adoptaremos la forma de prefijo.
El programa para contar caracteres acumula su cuenta en una
variable long en lugar de una int. Los
enteros long son por lo menos de 32 bits. Aunque en algunas
máquinas int y long son del mismo tamaño, en otras un
int es de 16 bits, con un valor máximo de 32767, y
tomaría relativamente poca lectura a la entrada para
desbordar un contador int. La especificación de
conversión %ld indica a printf que
el argumento correspondiente es un entero long.
Sería posible tener la capacidad de trabajar con
números mayores empleando un double
(float de doble precisión). También se
utilizará una proposición for en lugar
de un while, para demostrar otra forma de escribir el ciclo.
#include <stdio.h>
/* cuenta los caracteres de la
entrada; 2a. versión */
main()
{
double nc;
for (nc = 0; getchar() !=EOF; ++nc)
;
printf("%.0f\n", nc);
}
printf utiliza %f tanto para
float como para double; %.0f
suprime la impresión del punto decimal y de la parte
fraccionaria, que es cero.
El cuerpo de este ciclo for está
vacío, debido a que todo el trabajo se realiza en las
secciones de prueba e incremento. Pero las reglas gramaticales de C
requie- ren que una proposición for tenga un cuerpo. El
punto y coma aislado se llama proposición nula, y
está aquí para satisfacer este requisito. Lo
colocamos en una línea aparte para que sea visible.
Antes de abandonar el programa para contar caracteres,
obsérvese que si la entrada no contiene caracteres, la
prueba del while o del for no tiene éxito desde
la primera llamada a getchar, y el programa produce
cero, el resultado correcto. Esto es importante. Uno de los
aspectos agradables acerca del while y del
for es que hacen la prueba al inicio del ciclo, antes
de proceder con el cuerpo. Si no hay nada que hacer, nada se hace,
aun si ello significa no pasar a través del cuerpo del
ciclo. Los programas deben actuar en forma inteligente cuando se
les da una entrada de longitud cero. Las proposiciones while y for
ayudan a asegurar que los programas realizan cosas razonables con
condiciones de frontera.
El siguiente programa cuenta líneas a la entrada. Como se mencionó anteriormente, la biblioteca estándar asegura que una secuencia de texto de entrada parezca una secuencia de líneas, cada una terminada por un carácter nueva linea. Por lo tanto, contar lineas es solamente contar caracteres nueva linea:
#include <stdio.h>
/* cuenta las líneas
de la entrada */
main()
{
int c, nl;
nl = 0;
while ((c = getchar()) != EOF)
if (c == '\n')
++nl;
printf("%d\n", nl);
}
El cuerpo del while consiste ahora en un
if, el cual a su vez controla el incremento
++nl. La proposición if prueba la
condición que se encuentra entre paréntesis y, si la
condición es verdadera, ejecuta la proposición (o
grupo de proposiciones entre llaves) que le sigue. Hemos sangrado
nuevamente para mostrar lo que controla cada elemento. El doble
signo de igualdad == es la notación de C para
expresar "igual a" (como el = simple de Pascal o el
.EQ. de Fortran). Este símbolo se emplea para
distinguir la prueba de igualdad del = simple que
utiliza C para la asignación. Un mensaje de alerta: los
principiantes de C ocasionalmente escriben = cuando en
realidad deben usar ==. Como se verá en el
capítulo 2, el resultado es por lo general una
expresión legal, de modo que no se obtendrá ninguna
advertencia.
Un carácter escrito entre apóstrofos representa un
valor entero igual al valor numérico del carácter en
el conjunto de caracteres de la máquina. Esto se llama una
constante de carácter, aunque sólo es otra
forma de escribir un pequeño entero. Así, por ejemplo
'A' es una constante de carácter; en el
conjunto ASCII de caracteres su valor es 65, esto es, la
representación interna del carácter A.
Por supuesto 'A' es preferible que 65: su significado
es obvio, y es independiente de un conjunto de caracteres en
particular.
Las secuencias de escape que se utilizan en constantes de cadena
también son legales en constantes de carácter; asi,
'\n' significa el valor del carácter nueva
linea, el cual es 10 en código ASCII. Se debe notar
cuidadosamente que '\n' es un carácter simple,
y en expresiones es sólo un entero; por otro lado,
"\n" es una constante cadena que contiene sólo
un carácter. En el capítulo 2 se trata el tema de
cadenas versus caracteres.
Ejercicio 1-8. Escriba un programa que cuente espacios en
blanco, tabuladores y nuevas líneas.
Ejercicio 1.9. Escriba un programa que copie su entrada a
la salida, reemplazando cada cadena de uno o más blancos por
un solo blanco.
Ejercicio 1-10. Escriba un programa que copie su entrada
a la salida, reemplazando cada tabulación por
\t, cada retroceso por \b y cada diagonal
invertida por \\. Esto hace que las tabulaciones y los
espacios sean visibles sin confusiones.
El cuarto en nuestra serie de programas útiles cuenta las
líneas, palabras y caracteres, usando la definición
de que una palabra es cualquier secuencia de caracteres que no
contiene espacio en blanco ni tabulación ni nueva
línea. Esta es una versión reducida del programa
wc de UNIX.
#include <stdio.h>
#define IN 1 /* en una palabra */
#define OUT 0 /* fuera de una palabra */
/* cuenta líneas, palabras, y caracteres de la entrada */
main( )
{
int c, nl, nw, nc, state;
state = OUT;
nl = nw = nc = 0;
while ((c = getchar()) != EOF) {
++nc;
if (c == '\n')
++nl;
if (c == ' ' || c == '\n' ||
c == '\t')
state = OUT;
else if (state == OUT) {
state = IN;
++nw;
}
}
printf ("%d %d %d\n", nl, nw, nc);
}
Cada vez que el programa enceuntra el primer carácter de
una palabra, contabiliza una palabra más. La variable
state registra si actualmente el programa está
o no sobre una palabra; al inicio es "no está sobre una
palabra", por lo que se asigna el valor OUT. Es
preferible usar las constantes simbólicas IN y
OUT que los valores 1 y 0, porque hacen el programa
más legible. En un programa tan pequeña como
éste, la diferencia es mínima, pero en programas
más grandes el incremento en claridad bien vale el esfuerzo
extra que se haya realizado para escribir de esta manera desde el
principio. También se descubrirá que es más
facil hacer cambios extensivos en programas donde los
números mágicos aparecen sólo como constantes
simbólicas.
La linea
nl = nw = nc = 0;
inicializa a las tres variables en cero. Este no es un caso especial sino una consequencia del hecho de que una asignación es una expresión con un valor, y que las asignaciones se asocian de derecha a izquierda. Es como si se hubiese escrito
nl = (nw = (nc = 0));
El operador || significa "O" (o bien "OR"), por lo
que la línea
if (c == ' ' || c== '\n' || c== '\t')
dice "si c es un blanco o c es
nueva línea o c es un tabulador".
(Recuerde que la sequencia de escape \t es una
representación visible del carácter tabulador.)
Existe correspondente operador && para AND; su
precedencia es más alta que la de ||. Las
expresiones conectadas por && o
|| se evalúan de izquierda a derecha, y se
guarantiza que la evaluación terminará tan pronto
como se conozca la verdad o falsidad. Si c es blanco,
no hay necesidad de probar si es una nueva línea o un
tabulador, de modo que esas pruebas no se hacen. Esto no es de
partucular importancia en este caso, pero es significativo en
situaciones más complicadas, como se verá más
adelante.
El ejemplo muestra también un else, el cual
especifica una acción alternativa si la condición de
una proposición if es falsa. La forma general
es
if (expressión) proposición1 else proposición2
Una y sólo una de las dos proposiciones asociadas con un
if-else se realiza. Si la expresión es
verdadera, se ejecuta proposición1,
si no lo es, se ejecuta proposición2. Cada
proposición puede ser una proposición sencilla
o varias entre llaves. En un programa para contar palabras, la que
está después de else es un
if que controla dos proposiciones entre llaves.
Ejercicio 1-11. ¿Cómo probaría el
programa para contar palabras? ¿Qué clase de entrada
es la más conveniente para decubrir errores si éstos
existen?
Ejercicio 1-12. Escriba un programa que imprima su
entrada una palabra por línea.
Escribamos un programa para contar el número de ocurrencias de cada dígito, de caracteres espaciadores (blancos, tabuladores, nueva línea), y de todos los otros caracteres. Esto es artificioso, pero nos permite ilustrar varios aspectos de C en un programa.
Existen doce catagorías de entrada, por lo que es conveniente utilizar un arreglo para mantener el número de ocurrencias de cada dígito, en lugar de tener diez variables individuales. Esta es una versión del programa:
#include <stdio.h>
/* cuenta dígitos, espacios blancos, y otros */
main()
{
int c, i, nwhite, nother;
int ndigit[19];
nwhite = nother = 0;
for (i = 0; i < 10; ++i)
ndigit[i] = 0;
while ((c = getchar()) != EOF)
if (c >= '0' && c <= '9')
++ndigit[c-'0'];
else if (c == ' ' || c == '\n'
|| c == '\t')
++nwhite;
else
++nother;
printf ("dígitos =");
for (i = 0; i < 10; ++i)
printf(" %d", ndigit[i]);
printf(", espacios blancos = %d,
otros = %d\n", nwhite, nother);
}
La salida de este programa al ejecutarlo sobre si mismo es
dígitos = 9 3 0 0 0 0 0 0 0 1, espacios blancos = 123, otros = 345
La declaración
int ndigit[10];
declara ndigit como un arreglo de 10 enteros. En C,
los subíndices de arreglos comienzan en cero, por lo que los
elementos son ndigit[0], ndigit[1], ..., ndigit[9].
Esto se refleja en los ciclos for que inicializen e
imprimen el arreglo.
Un subíndicie puede ser cualquier expresión
entera, lo que incluye a variables enteras como i, y
constantes enteras.
Este programa en particular se basa en las propiedades de la representación de lo dígiros como caracteres. Por ejemplo, la prueba
if (c >= '0' && c <= '9') ...
determina si el carácter en c es un
dígito. Si lo es, el valor numérico del dígito
es
c - '0'
Esto sólo funciona si '0', '1', ..., '9'
tienen valores consecutivos ascendientes. Por fortuna, esto es
así en todos los conjuntos de caracteres.
Por definición, los char son
idénticas a las int en expresiones
aritméticas. Esto es natural y conveniente; por ejemplo,
c-'0' es una expresión entera con un valor
entre 0 y 9, correspondiente a los caracteres '0' a '9' almacenados
en c, por lo que es un subíndice válido
para el arreglo ndigit.
La decisión de si un carácter es un dígito, espacio en blanco u otra cosa se realiza con la secuencia
if (c >= '0' && c >= '9') ++ndigit[c-'0']; else if (c == ' ' || c == '\n' || c == '\t') ++nwhite; else ++nother;
El Patrón
if (condición1) proposición1 else if (condición2) proposición2 ... else proposiciónn
se encuentra frecuentemente en programas como una forma de
expresar una decisión múltiple. Las
condiciones se evalúan en orden desde el principio
hasta que se satisface alguna condición, en ese punto
se ejecuta la proposición correspondiente, y la
construcción completa termina. (Cualquier
proposición puede constar de varias proposiciones
entre llaves.) Si no se satisface ninguna de las condiciones, se
ejecuta la proposición que está después
del else final, si ésta existe. Cuando se
omiten el else y la proposición finales,
tal como se hizo en el programa para contar palabras, no tiene
lugar ninguna acción. Puede haber cualquier número de
grupos de
else if (condición) proposición
entre el if inicial y el else
final.
Se recomienda, por estilo, escribir esta construcción tal
como se ha mostrado; si cada if estuviese sangrado
después del else anterior, una larga secuencia
de decisiones podría rebasar el margen derecho de la
página.
La proposición switch que se trata en capítulo 3, propociona otra forma de
escribir una decisión múltiple, que es
particularmente apropiada cuando la condición es dterminar
si alguna expresión entera o de carácter corresponde
con algún miembro de un conjunto de constantes. Para
contrastar, se presentará una versión de este
programa, usando switch en la sección 3.4
Ejercicio 1-13. Escriba un programa que imprima el
histograma de las longitudes de las palabras de su entrada. Es
fácil dibujar el histograma con las barras horizontales; la
orientación vertical es un reto más interesante.
Ejercicio 1-14. Escriba un programa que imprima el
histograma de las frecuencias con que se presentan diferentes
caracteres leídos a la entrada.
En lenguaje C, una función es el equivalente a una subrutina o función en Fortran, o a un procedimiento o función en Pascal. Una función proporciona una forma conveniente de encapsular algunos cálculos, que se pueden emplear después sin preocuparse de sum implantación. Con funciones diseñadas adecuadamente, es posible ignorar cómo se realiza un trabajo; es suficiente saber qué se hace. El lenguaje C hace que el uso de funciones sea fácil, conveniente y eficiente; es común ver una función corta definida y empleada una sola vez, únicamente porque eso esclarece alguna parte del código.
Hasta ahora sólo se han utilizado funciones como
printf, getchar y putchar, que nos han
sido proporcionadas; ya es el momento de escribir unas pocas
nosotros mismos. Dado que C no posee un operador de
exponenciación como ** de Fortran, ilustremos el mecanismo
de la definición de una función al escribir la
función power(m,n), que eleva un entero
m a una potencia entera y positiva n.
Esto es, el valor de power(2,5) es 32. Esta
función no es una rutina de exponenciación
práctica, puesto que sólo maneja potencias positivas
de enteros pequeños, pero es suficiente para
ilustración (la biblioteca estándar contiene una
función pow(x,y) que calcula
xy).
A continuación se presenta la función
power y un programa main para utilizarla,
de modo que se vea la estructura completa de una vez
#include <stdio.h>
int power(int m, int n);
main()
{
int i;
for (i = 0; i < 10; ++i)
printf("%d %d %d\n", i, power(2,i), power(-3,i));
return 0;
}
/* power: eleva la base a la n-ésima potencia; n >= 0 */
int power(int base, int n)
{
int i, p;
p = 1;
for (i = 1; i <= n; ++i)
p = p * base;
return p;
}
Una definición de función tiene la forma siguiente:
tipo-de-retorno nombre-de-función
(declaración de parametros,
si los hay)
{
declaraciones
proposiciones
}
Las definiciones de función pueden aparecer en cualquier orden y en uno o varios archivos fuente, pero una función no puede separase en archivos diferentes. Si el programa fuente aparece en varios archivos, tal vez se tengan que especificar más cosas al compilar y cargarlo que si estuviera en uno solo, pero eso es cosa del sistema operativo, no un atributo del lenguaje. Por ahora supondremos que ambas funciones están en el mismo archivo y cualquier cosa que se haya aprendido acerca de cómo ejecutar programas en C, aún funcionaran.
La función power se invoca dos veces por
main, en la línea
printf("%d %d %d\n", i, power(2,i),
power(-3,i));
Cada llamada pasa dos argumentos a power, que cada
vez regresa un entero, al que se pone formato y se imprime. En una
expresión, power(2,i) es un entero tal como son
2 y i. (No todas las funciones producen
un valor entero; lo que se verá en al capítulo
4.)
La primera línea de la función
power,
int power(int base, int n)
declara los tipos y nombres de los parámetros, así
como el tipo de resultado que la función devuelve. Los
nombres que emplea power para sus parámetros
son locales a la función y son invisibles a cualquier otra
función: otras rutinas pueden utilizar los mismos nombres
sin que exista problema alguno. Esto también es cierto para
las variables i y p: la i de
power no tiene nada que ver con la i de
la main.
El valor que calcula power se regresa a
main por medio de la proposición
return, a la cual le puede seguir cualquier
expresión:
return expresión
Una función no necesita regresar un valor; una
proposición return sin expresión hace
que el control regrese al programa, pero no devuelve algún
valor de utilidad, com se haría al "caer al final" de una
función al alcanzar la llave derecha de terminación.
Adamás, la función que llama puede ignorarr el valor
que regresa una función.
Probablamente haya notado que hay una proposición
return al final de main. Puesto que
main es una función como cualquier otram
también puede regresar un valor a quien la invoca, que es el
medio ambiente en el que el programa se ejecuta.
Típicamente, un valor de regreso cero implica una
terminación normal; los valores diferentes de cero indican
condiciones de terminación no comunes o erróneas.
Para buscar la simplicidad, se han omitido hasta ahora las
proposiciones return de las funciones
main, pero se incluirán más adalante,
como un recordatorio de que los programas deben regresar su estado
final a su medio ambiente.
La declaración
int power(int m, int n);
precisamente antes de main, indica que
power es una función que espera dos argumentos
int y regresa un int. Esta
declaración, a la cual se le llama función
prototipo, debe coincidir con la definición y uso de
power. Es un error que la definición de una
función o cualquier uso que de ella se haga no corresponda
con su prototipo.
Los nombres de los parámetros no necesitan coincidir; de hecho, son optativos en el prototipo de una función, de modo que para el prototipo se pudo haber escrito
int power(int, int);
No obstante, unos nombres bien seleccionados son una buena documentación por lo que se emplearán frecuentamente.
Una nota histórica: La mayor modificación entre
ANSI C y las versiones anteriores es cómo están
declaradas las funciones. En la definición original de C, la
función power se pudo haber escrito de la
siguiente manera:
/* power: eleva la base a n-ésima
potencia; n >= 0 */
/* (versión en estilo antiguo) */
power(base, n)
int base, n;
{
int i, p;
p = 1;
for (i = 1; i <= n; ++i)
p = p * base;
return p;
}
Los parámetros se nombran entre los paréntesis y
sus tipos se declaran antes de abrir la llave izquierda; los
parámetros que no se declaran se toman como
int. (El cuerpo de la función es igual a la
anterior.)
La declaración de power al inicio del
programa pudo haberse visto como sigue:
int power();
No se permitió ninguna lista de parámetros, de
modo que el compilador no pudo revisar con facilidad que
power fuera llamada correctamente. De hecho, puesto
que por omisión se podía suponer que
power regresaba un entero, toda la declaración
podría haberse omitido.
La nueva sintaxis de los prototipos de funciones permite que sea mucho más fácil para el compilador detectar errores en el número o tipo de argumentos. El viejo estil de declaración y definición aún funciona en ANSI C, al menos por un periodo de transición, pero se recomienda ampliamente que se utilice la nueva forma si se tiene un compilador que la maneje.
Ejercicio 1-15. Escriba de nuevo el programa de
conversión de temperatura de la sección 1.2, de modo
que utilice una función para la conversión.
Hay un aspecto de las funciones de C que puede paracer poco
familiar a los programadores acostumbrados a otros lenguajes,
particularmente Fortran. En C, todos los argumentos de una
función se pasan "por valor". Esto significa que la
función que se invoca recibe los valores de sus argumentos
en variables temporales y no en las originales. Esto conduce a
algunas propiedades diferentes a las que se ven en lenguajes con
"llamadas por referencia" como Fortran o con parámetros
var en Pascal, en donde la rutina que se invoca tiene
acceso al argumento original, no a una copia local.
La diferencia principal es que en C la función que se invoca no puede alterar directamente una variable de la función que hace la llamada; sólo puede modificar su copia privada y temporal.
Sin embargo, la llamada por valor ed una ventaja, no una
deventaja. Por lo común, esto conduce a elaborar programas
más compactos con pocas variables extrañas, debido a
que los parámetros se tratan el la función invocada
como variables locales convenientemente inicializadas. Por ejemplo,
he aquí una versión de power que utiliza
esta propiedad.
/* power: eleva la base a la n-ésima
potencia; n >=0; versión 2 */
int power(int base, int n)
{
int p;
for (p = 1; n > 0; --n)
p = p * base;
return p;
}
El parámetro n se utiliza como una variable
temporal, y se decrementa (un ciclo for se ejecuta
hacia atrás) hasta que llega a cero; ya no es necesaria la
variable i. Cualquier cosa que se le haga a
n dentro de power no tiene efecto sobre
el argumento con el que se llamó originalmente
power.
Cuando sea necesario, es posible hacer que una función modifique una variable dentro de una rutina invocada. La función que llama debe proporcionar la dirección de la variable que será cambiada (técnicamente un apuntador a la variable), y la función que se invoca debe declarar que el parámetro sea un apuntador y tenga acceso a la variable indirectamente a través de él. Los apuntadores se tratarán en el capítulo 5.
La historia es diferente con los arreglos. Cuando el nombre de un arreglo se emplea como argumento, el valor que se pasa a la función es la localización del principio del arreglo -- no hay copia de los elementos del arreglo. Al colocarle subíndices a este valor, la función puede tener acceso y alterar cualquier elemento del arreglo. Este es el tema de la siguiente sección.
El tipo de arreglo más común en C es el de caracteres. Para ilustrar el uso de arreglos de caracteres y funciones que los manipulan, escriba un programa que lea un conjunto de líneas de texto e imprima la de mayor longitud. El pseudocódigo es bastate simple:
while (hay otra línea)
if (es más larga que la
anterior más larga)
guárdala
guarda su longitud
imprime la línea más larga
Este pseudocódigo deja en claro que el programa se divide naturalmente en partes. Una trae una nueva línea, otra la prueba y el resto controla el proceso.
Puesto que la división de las partes es muy fina, lo
correcto será escribirlas de ese modo. Así pues,
escribamos primero una función getline para
extraer la siguiente línea de la entrada. Trataremos de
hacer a la función útil en otros contextos. Al menos
getline tiene que regresar una señal acerca de
la posibilidad de un fin de archivo; un diseño de más
utilidad deberá retornar la longitud de la línea, o
cero si se encuentra el fin de archivo. Cero es un regreso de fin
de archivo aceptable debido a que nunca es una longitud de
línea válida. Cada línea de texto tiene al
menos un carácter; incluso una línea que sólo
contenga un carácter nueva línea tiene longitud
1.
Cuando se encuentre una línea que es mayor que la
anteriormente más larga, se debe guardar en algún
lugar. Esto sugiere una segunda función copy,
para copiar la nueva línea a un lugar seguro.
Finalmente, se necesita un programa principal para controlar
getline y copy. El resultado es el
siguiente:
#include <stdio.h>
#define MAXLINE 1000 /* tamaño
máximo de la
línea de
entrada */
int getline(char line[],int maxline);
void copy(char to[], char from[]);
/* imprime la línea de entrada
más larga */
main()
{
int len; /* longitud
actual de
la línea */
int max; /* máxima
longitud vista
hasta el
momento */
char line[MAXLINE]; /* línea
de entrada
actual */
char longest[MAXLINE]; /* la línea
más larga se
guarda aquí */
max = 0;
while ((len =
getline(line, MAXLINE)) > 0)
if (len > max) {
max = len;
copy(longest, line);
}
/* hubo una línea */
if (max > 0)
printf("%s", longest);
return 0;
}
/* getline: lee una línea en s,
regresa su longitud */
int getline(char s[], int lim)
{
int c, i;
for (i=0;
i<lim-1 && (c=getchar()) !=EOF
&& c!= '\n'; ++i)
s[i] = c;
if (c == '\n') {
s[i] = c;
++i;
}
s[i] = '\0';
return i;
}
/*copy: copia 'from' en 'to'; supone
que to es suficientemante grande */
void copy(char to[], char from[])
{
int i;
i = 0;
while((to[i] = from[i]) != '\0')
++i;
}
Las funciones getline y copy
están declaradas al principio del programa, que se supone
está contenido en un archivo.
main y getline se comunican a
través de un par de argumentos y un valor de retorno. En
getline los argumentos se declararan por la
línea
int getline(char s[], int lim)
que especifica que el primer argumento, s, es un
arreglo, y el segundo, lim, es un entero. El
propósito de proporcionar el tamaño de un arreglo es
fijar espacio de almacenamiento contiguo. La longitud del arreglo
s no es necesariamente en getline, puesto
que su tamaño se fija en main. En
getline se utiliza return para regresar
un valor a quién lo llama, tal como hizo la función
power. Esta línea también declara que
getline regresa un int; puesto que
int es el valor de retorno por omisión, puede
suprimirse.
Algunas funciones regresan un valor útil; otras, como
copy, se emplean únicamente por su efecto y no
regresan un valor. El tipo de retorno de copy es
void, el cual establece explícitamente que
ningún valor se regresa.
En getline se coloca el carácter '\0'
(carácter nulo, cuyo valor es cero) al final del
arreglo que está creando, para marcar el fin de la cadena de
caracteres. Esta convención también se utiliza por el
lenguaje C: cuando una contante de carácter como
"hola\n"
aparece en un programa en C, se almacena como un arreglo que contiene los caracteres de la cadena y termina con un '\0' para marcar el fin.
La especificación de formato %s dentro de
printf espera que el argumento correspondiente sea una
cadena representada de este modo: copy también
se basa en el hecho de que su argumento de entrada se termina con
'\0', y copia este carácter dentro del argumento de salida.
(Todo esto implica que '\0' no es parte de un texto normal.)
Es útil mencionar de paso que aun un programa tan
pequeño como éste presenta algunos problemas de
diseño. Por ejemplo, ¿qué debe hacer
main si encuentra una línea que es mayor que su
límite? getline trabaja en forma segura, en ese
caso detiene la recopilación cuando el arreglo está
lleno, aunque no encuentre el carácter nuevo línea.
Probando la longitud y el último carácter devuelto,
main puede determinar si la línea fue demasiado
larga, y entonces realiza el tratamiento que se desee. Por
brevidad, hemos ignorado el asunto.
Para un usuario de getline no existe forma de saber
con anticipación cuán larga podrá ser una
línea de entrada, por lo que getline revisa un
posible desbordamiento (overflow). Por otro lado, el usuario
de copy ya conoce (o lo puede averiguar) cuál es el
tamaño de la cadena, por lo que decidimos no agregar
comprobación de errores en ella.
Ejercicio 1-16. Corija la rutina principal del
programa de la línea más larga de modo que imprima
correctamente la longitud de líneas de entrada
arbitrariamente largas, y tanto texto como sea posible.
Ejercicio 1-17. Escriba un programa que
imprima todas las líneas de entrada que sean mayores de 80
caracteres.
Ejercicio 1-18. Escriba un programa que
elimine los blancos y los tabuladores que estén al final de
cada línea de entrada, y que borre completamente las
líneas en blanco.
Ejercicio 1-19. Escriba una función
reverse(s) que inverta la cadena de caracteres
s. Usela para escribir una programa que inverta su
entrada, línea a línea.
Las variables que están en main, tal como
line, longest, etc., son privadas o
locales a ella. Debido a que son declaradas dentro de
main, ninguna otra función puede tener aceso
directo a ellas. Lo mismo también es válido para
variables de otras funciones; por ejemplo, la variable
i en getline no tiene relación con
la i que está en copy. Cada
variable local de una función comienza a existir sólo
cuando se llama a la funciónm y desaparece cuando la
función termina. Esto es por lo que tales variables son
conocidas como variables automáticas, siguiendo la
terminología de otros lenguajes. Aquí se
utilizará en adelante el termino automático para
hacer referencia a esas variables locales. (En el capítulo 4
se discute la categoría de almacenamiento estática,
en la que las variables locales sí conservan sus valores
entre llamadas.)
Debido a que las variables locales aparecen y desaparecen con la invocación de funciones, no retienen sus valores entre dos llamadas sucesivas, y deben ser inicializadas explícitamente en cada entrada. De no hacerlo, contendrán "basura".
Como una alternativa a las variables automáticas, es posible definir variables que son externas a todas las funciones, esto es, variables a las que toda función puede tener acceso por su nombre. (Este mecanismo es paracido al COMMON de Fortran o a las variables de Pascal declaradas en el bloque más exterior.) Debiso a que es posible tener acceso global a las cariables externas, éstas pueden ser usadas en lugar de listas de argumentos para comunicar datos entre funciones. Además, puesto que las variables externas se mantienen permanentamente en existencia, en lugar de aparacer y desaparecer cuando se llaman y terminan las funciones, mantienen sus valores aun después de que regresa la función que los fijo.
Una variable externa debe definirse, exactamente una vez,
fuera de cualquier función; esto fija un espacio de
almacenamiento para ella. La variable también debe
declararse en cada función que desee tener acceso a
ella; esto establece el tipo de la variable. La declaración
debe ser una proposición extern
explícita, o bien puede estar implícita en el
contexto. Para concretar la discusión, reescribamos el
programa de la línea más larga con line,
longest y max como variables externas.
Esto requiere cambiar las llamadas, declaraciones y cuerpos de las
tres funciones.
#include <stdio.h>
#define MAXLINE 1000 /* máximo
tamaño de
una línea
de entrada */
int max; /* máxima
longitud vista
hasta el
momento */
char line[MAXLINE]; /* línea de
entrada
actual */
char longest[MAXLINE]; /* la línea
más larga se
guarda aquí */
int getline(void);
void copy(void);
/* imprime la línea de entrada más
larga; versión especializada */
main()
{
int len;
extern int max;
extern char longest[];
max = 0;
while ((len = getline()) > 0)
if (len > max) {
max = len;
copy();
}
if (max > 0) /* hubo una línea */
printf("%s", longest);
return 0;
}
/* getline: versión especializada */
int getline(void)
{
int c, i;
extern char line[];
for (i = 0; i < MAXLINE-1 &&
(c=getchar()) != EOF && c!= '\n';
++i)
line[i] = c;
if (c == '\n') {
line[i] = c;
++i;
}
line[i] = '\0';
return i;
}
/* copy: versión especializada */
void copy(void)
{
int i;
extern char line[], longest[];
i = 0;
while ((longest[i] = line[i])
!= '\0')
++i;
}
Las variables externas de main,
getline, y copy están definidas en
las primeras líneas del ejemplo anterior, lo que establece
su tipo y causa que se les asigne espacio de amacenamiento. Desde
el punto de vista sintáctico, las definiciones externas son
exactamente como las definiciones de variables locales, pero puesto
que occurren fuera de las funciones, las variables son externas.
Antes de que una función pueda usar una variable externa, se
debe hacer saber el nombre de la variable a la función. Una
forma de hacer esto es escribir una declaración
extern dentro de la función; la
declaración es la misma que antes, excepto por la palabra
reservada extern.
Bajo ciertas circunstancias, la declaración
extern se puede omitir. Si la declaración de
una variable externa ocurre dentro del archivo fuente antes de su
uso por una función en particular, entonces no es necesario
el uso de una declaración extern dentro de la
función. La declaración extern en
main, getline y copy es, por
lo tanto, redundante. De hecho, una práctica común,
es poner las definiciones de todas las variables externas al
principio del archivo fuente y después onitir todas las
declaraciones extern.
Si el programa está en varios archivos fuente y una
variable se define en archivo1 y se utiliza en
archivo2 y archivo3, entonces se necesitan
declaraciones extern en archivo2 y
archivo3 para conectar las ocurrencias de la variable. La
práctica común es reunir declaraciones
extern de variables y funciones en un archivo
separado, llamado históricamente header, que es
incluido por #include al principio de cada archivo
fuente. El sufijo .h se usa por convención para nombres de
headers. Las funciones de la biblioteca
estándar, por ejemplo, están declaradas en
headers como <stdio.h>. Este tema se
trata ampliamente en el capítulo 4, y
la biblioteca en el capítulo 7 y en el
apéndice B.
Puesto que las versiones especializadas de getline
y copy no tienen argumentos, la lógica
sugeriría que sus prototipos al principio de archivo deben
ser getline() y copy(). Pero por
compatibilidad con programas de C anteriores, el estándar
toma a una lista vacía como una declaración al viejo
estilo, y suspende toda revisión de listas de argumentos;
para una lista explícitamente vacía debe emplearse la
palabra void. Esto se discutirá en el capítulo 4.
Se debe notar que empleamos cuidadosamente las palabras definición y declaración cuando nos referimos a variables externas en está sección. La palabra "definición" se refiere al lugar donde se crea la variable o se le asigna un lugar de almacenamiento; "declaración" se refiere al lugar donde se establece la naturaleza de la variable pero no se le asigna espacio.
A propósito, existe una tendencia a convertir todo en
variables extern, debido a que aparentamente
simplifica las comunicaciones --las listas de argumentos son cortas
y las variables existen siempre, aun cuando no hacen falta.
Descansar fuertamente sobre variables externas es peligroso, puesto
que lleva a programas cuyas conexiones entre datos no son
completamente obvias --las variables pueden cambiarse en forma
inesperada e inadvertida, y el programa es difícil de
modificar. La segunda versión del programa de la
línea mayor es inferior a la primera, en parte por las
anteriores razones y en parte porque destruye la generalidad de dos
útiles funciones, introduciendo en ellas los nombres de las
variables que manipula.
Hasta este punto hemos descrito lo que podría llamarse los fundamentos convencionales de C. Con estos fundamentos, es posible escribir programas útiles de tamaño considerable, y probablamente sería una buena idea hacer una pausa suficientemente grande para realizarlos. Estos ejercicios sugieren programas de complejidad algo mayor que los anteriores del capítulo.
Ejercicio 1-20. Escriba un programa
detab que reemplace tabuladores de la entrada con el
número apropiado de blancos para espaciar hasta el siguiente
paro de tabulación. Considere en conjunto fijo de paros de
tabulación, digamos cada n columnas. ¿Debe se
n una variable o un parámetro simbólico?
Ejercicio 1-21. Escriba un programa
entab que reemplace cadenas de blancos por el
mínimo número de tabuladores y blancos para obtener
el mismo espaciado. Considere los paros de tabulación de
igual manera que para detab. Cuando un tabulador o un
simple espacio en blanco fuese suficiente para alcanzar un paro de
tabulación, ¿a cuál se le debe dar
preferencia?
Ejercicio 1-22. Escriba un programa para
"doblar" líneas grandes de entrada en dos o más
líneas cortas después del último
carácter no blanco que ocurra antes de la
n-ésima columna de entrada. Asegúrese de que
su programa se comporte apropiadamente con líneas muy
largas, y de que no hay blancos o tabuladores antes de la columna
especificada.
Ejercicio 1-23. Escriba un programa para
eliminar todos los comentarios de un programa en C. No olivide
manejar apropiadamente las cadenas entre comillas y las constantes
de carácter. Los comentarios de C no se anidan.
Ejercicio 1-24. Escriba un programa para
revisar los errores de sintaxis rudimentarios de un programa en C,
como paréntesis, llaves y corchetes no alineados. No olvide
las comillas ni los apóstrofos, las secuencias de escape y
los comentarios. (Este programa es difícil si se hace
completamente general.)